package dryven.request.routing.routers;

import java.lang.reflect.Method;
import java.util.Locale;
import java.util.Set;

import dryven.discovery.ClassDiscoveryListener;
import dryven.discovery.annotation.Annotation;
import dryven.reflection.DeclaringMethodIterator;
import dryven.reflection.TypeRef;
import dryven.request.controller.ControllerDescription;
import dryven.request.controller.paramtransform.ActionParameterSerializer;
import dryven.request.controller.paramtransform.ActionParameterTransformResult;
import dryven.request.controller.paramtransform.ActionParameterTransformationException;
import dryven.request.controller.paramtransform.ActionParameterTransformer;
import dryven.request.http.Request;
import dryven.request.http.Response;
import dryven.request.routing.Router;
/**
 * /list/list
 * 
 * list/list/IndexController:index	(ic,ia)
 * list/ListController:index		(ia)
 * list/IndexController:list		(ic)
 * ListController:list				()
 * 
 * list/IndexController:index 		(ic,ia)
 * ListController:index				(ia)
 * IndexController:list				(ic)
 * --ListController:list			(N/A)
 * */
public class PackageRouter implements Router {
	
	public static final String ControllerClassSuffix = "Controller";
	public static final String IndexControllerName = "Index";
	public static final String ControllerIndexActionName = "index";
	
	private Iterable<String> rootControllerPackages;

	public PackageRouter(Iterable<String> rootControllerPackages) {
		super();
		this.rootControllerPackages = rootControllerPackages;
	}

	@Override
	public ControllerDescription routeToController(Request req, Response response, ActionParameterTransformer paramTransformer) {
		String[] routeParts = parseRoute(req.getLocalApplicationURL());
		//and look for matching controllers
		for (String rootPackage : rootControllerPackages) {
			ControllerDescription cd = createControllerDescriptionForRootPackage(rootPackage, routeParts, paramTransformer, req, response);
			if(cd!=null) {
				return cd;
			}
		}
		return null;
	}

	private String[] parseRoute(String route) {
		String[] routeParts = null;
		if(route.length()!=0) {
			routeParts = route.substring(1).split("/");
		} else {
			return new String[0];
		}
		int realSize = routeParts.length;
		//check for empty parts and ignore them
		for(int i=0;i<routeParts.length;++i) {
			if(routeParts[i].length()==0) {
				--realSize;
			}
		}
		if(realSize!=routeParts.length) {
			String[] realParts = new String[realSize];
			if(realSize!=0) {
				int realIndex = 0;
				for(int i=0;i<routeParts.length;++i) {
					if(routeParts[i].length()!=0) {
						realParts[realIndex]=routeParts[i];
						++realIndex;
					}
				}
			}
			routeParts = realParts;
		}
		return routeParts;
	}
	
	private ControllerDescription createControllerDescriptionForRootPackage(String rootPackage, String[] route, ActionParameterTransformer paramTransformer, Request req, Response response) {
		//search for route paths that cannot be part of the fully qualified router name and are therefor parameters
		int firstObligedParameterIndex = route.length;
		for(int i=route.length-1;i>=0;--i) {
			if(!isValidIdentifier(route[i])) {
				firstObligedParameterIndex = i;
			}
		}
		//handle /
		if(route.length==0) {
			ControllerDescription cd = tryController(rootPackage, route, -1, true, true, paramTransformer, req, response);
			return cd;
		}
		for(int i=firstObligedParameterIndex-1;i>=0;--i) {
			ControllerDescription cd = null;
			//if(route.length>1) {
				cd = tryController(rootPackage, route, i, true, true, paramTransformer, req, response);
				if(cd!=null) {
					return cd;
				}
			//}
			cd = tryController(rootPackage, route, i, false, true, paramTransformer, req, response);
			if(cd!=null) {
				return cd;
			}
			cd = tryController(rootPackage, route, i, true, false, paramTransformer, req, response);
			if(cd!=null) {
				return cd;
			}
			//check bounds of route
			if(route.length>=2 && i>0) {
				cd = tryController(rootPackage, route, i, false, false, paramTransformer, req, response);
				if(cd!=null) {
					return cd;
				}
			}
		}
		//special case: root index controller (/) with parameters
		ControllerDescription cd = tryController(rootPackage, route, -1, true, true, paramTransformer, req, response);
		if(cd!=null) {
			return cd;
		}
		return null;
	}
	
	private ControllerDescription tryController(String rootPackage, String[] route, int actionIndex, boolean indexController, boolean indexAction, ActionParameterTransformer paramTransformer, Request req, Response response) {
		String action = ControllerIndexActionName;
		String controllerBase = IndexControllerName;
		int packageEnd = actionIndex-2;
		if(indexAction) {
			++packageEnd;
		} else {
			action = route[actionIndex];
		}
		if(indexController) {
			++packageEnd;
		} else {
			controllerBase = route[packageEnd+1];
		}
		String className = getFullyQualifiedClassNameForRouteOffset(rootPackage, route, packageEnd, controllerBase);
		ControllerDescription cd = createControllerDescriptionForClass(className, action, route, actionIndex+1, paramTransformer, req, response);
		return cd;
	}
	
	private ControllerDescription createControllerDescriptionForClass(String className, String actionName, String[] route, int firstParameterIndex, ActionParameterTransformer paramTransformer, Request req, Response response) {
		try {
			Class<?> controllerClass = Class.forName(className);
			int parameterCount = route.length-firstParameterIndex;
			
			String[] paramsStringValues = new String[parameterCount];
			if(parameterCount!=0) {
				System.arraycopy(route, firstParameterIndex, paramsStringValues, 0, paramsStringValues.length);
			}
			ControllerDescription cd = selectBestControllerDescription(controllerClass, actionName, paramsStringValues, paramTransformer, req, response);
			//can be null
			return cd;
		} catch (ClassNotFoundException e) {}
		return null;
	}
	
	private ControllerDescription selectBestControllerDescription(Class<? extends Object> controllerClass, String actionName, String[] parameters, ActionParameterTransformer paramTransformer, Request req, Response response) {
		PackageRouterTransformResult bestResult = null;
		ActionParameterTransformationException currentException = null, bestResultException = null;
		for (Method m : new DeclaringMethodIterator(controllerClass)) {
			if(
				(m.getName().toLowerCase().equals(actionName.toLowerCase()) ||
				m.getName().toLowerCase().equals(actionName.toLowerCase()+"_")) &&
				parameters.length<=m.getParameterTypes().length
					
			) {
				PackageRouterTransformResult result = new PackageRouterTransformResult(m);
				try {
					result = (PackageRouterTransformResult)paramTransformer.transform(m, parameters, result, req, response);
					currentException = null;
				} catch(ActionParameterTransformationException e) {
					currentException = e;
				}
				if(bestResult==null || bestResult.getOptionalFilledParameterCount()>result.getOptionalFilledParameterCount()) {
					bestResult = result;
					bestResultException = currentException;
				}
			}
		}
		if(bestResultException!=null) {
			throw bestResultException;
		}
		if(bestResult!=null) {
			return new ControllerDescription(controllerClass, bestResult.getAction(), bestResult.getParameters());
		}
		return null;
	}
	
	private static String getFullyQualifiedClassNameForRouteOffset(String rootPackage, String[] route, int packageEnd, String classBase) {
		StringBuilder buf = new StringBuilder(rootPackage);
		for(int i=0;i<=packageEnd;++i) {
			buf.append('.');
			buf.append(route[i].toLowerCase(Locale.ENGLISH));
		}
		buf.append('.');
		String className = classBase.substring(0, 1).toUpperCase(Locale.ENGLISH)+classBase.substring(1)+ControllerClassSuffix;
		buf.append(className);
		return buf.toString();
	}
	
	private boolean isValidIdentifier(String str) {
		if(str.length()==0) {
			return false;
		}
		if(!Character.isJavaIdentifierStart(str.charAt(0))) {
			return false;
		}
		for(int i=1;i<str.length();++i) {
			if(!Character.isJavaIdentifierPart(str.charAt(i))) {
				return false;
			}
		}
		return true;
	}
	
	@Override
	public String controllerToRoute(ControllerDescription cd, ActionParameterTransformer paramTransformer) {
		Class<?> controllerClass = cd.getControllerClass();
		
		if(!controllerClass.getName().endsWith(ControllerClassSuffix)) {
			return null;
		}
 		
		String controllerPackage = controllerClass.getPackage().getName();
		String basePackage = null;
		for (String packageName : rootControllerPackages) {
			if(controllerPackage.startsWith(packageName)) {
				basePackage = packageName;
			}
		}
		//controller package does not match any of the packages this router is assigned to
		if(basePackage==null) {
			return null;
		}
		
		String fullClassName = controllerClass.getName();
		String relativeClassName = fullClassName.substring(basePackage.length()+1);
		String parts[] = relativeClassName.split("\\.");
		
		StringBuilder buffer = new StringBuilder();
		//package path parts
		for(int i=0;i<parts.length-1;++i) {
			buffer.append('/');
			buffer.append(parts[i]);
		}
		//controller class path part
		String controllerPart = parts[parts.length-1];
		controllerPart = controllerPart.substring(0, 1).toLowerCase(Locale.ENGLISH)+controllerPart.substring(1, controllerPart.length()-ControllerClassSuffix.length());
		if(!controllerPart.equals(IndexControllerName.toLowerCase(Locale.ENGLISH))) {
			buffer.append('/');
			buffer.append(controllerPart);
		}
		//method path part
		String methodName = cd.getAction().getName();
		if(methodName.startsWith("_")) {
			methodName = methodName.substring(1);
		}
		if(!methodName.equals(ControllerIndexActionName)) {
			buffer.append('/');
			buffer.append(methodName);
		}
		
		ActionParameterSerializer serializers[] = paramTransformer.getParameterSerializers(cd.getAction());
		for (int i=0;i<cd.getParameterCount();++i) {
			Object value = cd.getActionParameter(i);
			ActionParameterSerializer serializer = serializers[i];
			if(value==null && serializer!=null) {
				throw new RuntimeException("Required parameter is missing");
			}
			if(serializer!=null) {
				buffer.append('/');
				buffer.append(serializer.serializeParameter(value));
			}
		}
		return buffer.toString();
	}
}

class PackageRouterTransformResult implements ActionParameterTransformResult {
	
	public PackageRouterTransformResult(Method action) {
		super();
		this.action = action;
	}

	private Method action;
	private int totalOptionalParameterCount = 0, optionalFilledParameterCount = 0;
	private Object[] parameters;
	
	@Override
	public void setOptionalFilledParameterCount(int count) {
		optionalFilledParameterCount = count;
	}

	@Override
	public void setTotalOptionalParameterCount(int count) {
		totalOptionalParameterCount = count;
	}

	@Override
	public void setParameters(Object[] o) {
		parameters = o;
	}

	public int getTotalOptionalParameterCount() {
		return totalOptionalParameterCount;
	}

	public int getOptionalFilledParameterCount() {
		return optionalFilledParameterCount;
	}

	public Object[] getParameters() {
		return parameters;
	}

	public Method getAction() {
		return action;
	}
}
